frontend/pages/e/[uuid]/details.tsx (view raw)
1import moment from 'moment';
2import Linkify from 'linkify-react';
3import Tooltip from '@mui/material/Tooltip';
4import IconButton from '@mui/material/IconButton';
5import Box from '@mui/material/Box';
6import Link from '@mui/material/Link';
7import Card from '@mui/material/Card';
8import Container from '@mui/material/Container';
9import TextField from '@mui/material/TextField';
10import Typography from '@mui/material/Typography';
11import TuneIcon from '@mui/icons-material/Tune';
12import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
13import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
14import {useTheme} from '@mui/material/styles';
15import {DatePicker} from '@mui/x-date-pickers/DatePicker';
16import {PropsWithChildren, useState} from 'react';
17import {useTranslation} from 'next-i18next';
18import pageUtils from '../../../lib/pageUtils';
19import DetailsLink from '../../../containers/DetailsLink';
20import ShareEvent from '../../../containers/ShareEvent';
21import PlaceInput from '../../../containers/PlaceInput';
22import LangSelector from '../../../components/LangSelector';
23import usePermissions from '../../../hooks/usePermissions';
24import useEventStore from '../../../stores/useEventStore';
25import useToastStore from '../../../stores/useToastStore';
26import EventLayout, {TabComponent} from '../../../layouts/Event';
27import {
28 EventByUuidDocument,
29 useUpdateEventMutation,
30} from '../../../generated/graphql';
31import {langLocales} from '../../../locales/constants';
32
33interface Props {
34 eventUUID: string;
35 announcement?: string;
36}
37
38const Page = (props: PropsWithChildren<Props>) => {
39 return <EventLayout {...props} Tab={DetailsTab} />;
40};
41
42const DetailsTab: TabComponent<Props> = ({}) => {
43 const {t} = useTranslation();
44 const {
45 userPermissions: {canEditEventDetails},
46 } = usePermissions();
47 const theme = useTheme();
48 const [updateEvent] = useUpdateEventMutation();
49 const addToast = useToastStore(s => s.addToast);
50 const setEventUpdate = useEventStore(s => s.setEventUpdate);
51 const event = useEventStore(s => s.event);
52 const [isEditing, setIsEditing] = useState(false);
53
54 if (!event) return null;
55
56 const hasGeoloc = event.latitude && event.longitude;
57
58 const onSave = async e => {
59 try {
60 const {uuid, ...data} = event;
61 const {
62 id,
63 travels,
64 waitingPassengers,
65 __typename,
66 administrators,
67 passengers,
68 ...input
69 } = data;
70 await updateEvent({
71 variables: {
72 uuid,
73 eventUpdate: {
74 ...input,
75 },
76 },
77 refetchQueries: ['eventByUUID'],
78 });
79 setIsEditing(false);
80 } catch (error) {
81 console.error(error);
82 addToast(t('event.errors.cant_update'));
83 }
84 };
85
86 const modifyButton = isEditing ? (
87 <Tooltip
88 title={t('event.details.save')}
89 sx={{
90 position: 'absolute',
91 top: theme.spacing(2),
92 right: theme.spacing(2),
93 }}
94 >
95 <IconButton color="primary" onClick={onSave}>
96 <CheckCircleOutlineIcon />
97 </IconButton>
98 </Tooltip>
99 ) : (
100 <Tooltip
101 title={t('event.details.modify')}
102 sx={{
103 position: 'absolute',
104 top: theme.spacing(2),
105 right: theme.spacing(2),
106 }}
107 >
108 <IconButton color="primary" onClick={() => setIsEditing(true)}>
109 <TuneIcon />
110 </IconButton>
111 </Tooltip>
112 );
113
114 return (
115 <Box
116 sx={{
117 position: 'relative',
118 }}
119 >
120 <Container
121 sx={{
122 p: 4,
123 mt: 6,
124 mb: 11,
125 mx: 0,
126 [theme.breakpoints.down('md')]: {
127 p: 2,
128 },
129 }}
130 >
131 <Card
132 sx={{
133 position: 'relative',
134 maxWidth: '100%',
135 width: '480px',
136 p: 2,
137 }}
138 >
139 <Typography variant="h4" pb={2}>
140 {t('event.details')}
141 </Typography>
142 {canEditEventDetails() && modifyButton}
143 {(isEditing || event.name) && (
144 <Box pt={2} pr={1.5}>
145 <Typography variant="overline">
146 {t('event.fields.name')}
147 </Typography>
148 <Typography>
149 {isEditing ? (
150 <TextField
151 size="small"
152 fullWidth
153 value={event.name}
154 onChange={e => setEventUpdate({name: e.target.value})}
155 name="name"
156 id="EditEventName"
157 />
158 ) : (
159 <Typography id="EventName">{event.name}</Typography>
160 )}
161 </Typography>
162 </Box>
163 )}
164 {(isEditing || event.date) && (
165 <Box pt={2} pr={1.5}>
166 <Typography variant="overline">
167 {t('event.fields.date')}
168 </Typography>
169 {isEditing ? (
170 <Typography>
171 <DatePicker
172 slotProps={{
173 textField: {
174 size: 'small',
175 id: `EditEventDate`,
176 fullWidth: true,
177 placeholder: t('event.fields.date_placeholder'),
178 },
179 }}
180 format="DD/MM/YYYY"
181 value={moment(event.date)}
182 onChange={date =>
183 setEventUpdate({
184 date: !date ? null : moment(date).format('YYYY-MM-DD'),
185 })
186 }
187 />
188 </Typography>
189 ) : (
190 <Box position="relative">
191 <Typography id="EventDate">
192 {moment(event.date).format('DD/MM/YYYY')}
193 </Typography>
194 </Box>
195 )}
196 </Box>
197 )}
198 {(isEditing || event.address) && (
199 <Box pt={2} pr={1.5}>
200 <Typography variant="overline">
201 {t('event.fields.address')}
202 </Typography>
203 {isEditing ? (
204 <PlaceInput
205 place={event.address}
206 latitude={event.latitude}
207 longitude={event.longitude}
208 onSelect={({place, latitude, longitude}) =>
209 setEventUpdate({
210 address: place,
211 latitude,
212 longitude,
213 })
214 }
215 />
216 ) : (
217 <Box position="relative">
218 <Typography
219 id="EventAddress"
220 title={t`placeInput.noCoordinates`}
221 sx={{
222 pr: 3,
223 display: 'inline-flex',
224 alignItems: 'center',
225 columnGap: 1,
226 }}
227 >
228 <Link
229 target="_blank"
230 rel="noreferrer"
231 href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
232 event.address
233 )}`}
234 onClick={e => e.preventDefault}
235 >
236 {event.address}
237 </Link>
238 {!hasGeoloc && (
239 <InfoOutlinedIcon fontSize="small" color="warning" />
240 )}
241 </Typography>
242 </Box>
243 )}
244 </Box>
245 )}
246 {(isEditing || event.description) && (
247 <Box pt={2} pr={1.5}>
248 <Typography variant="overline">
249 {t('event.fields.description')}
250 </Typography>
251 {isEditing ? (
252 <Typography>
253 <TextField
254 fullWidth
255 multiline
256 maxRows={4}
257 inputProps={{maxLength: 250}}
258 value={event.description || ''}
259 onChange={e =>
260 setEventUpdate({description: e.target.value})
261 }
262 id={`EditEventDescription`}
263 name="description"
264 />
265 </Typography>
266 ) : (
267 <Typography
268 id="EventDescription"
269 sx={{pr: 3, whiteSpace: 'pre-line'}}
270 >
271 <Linkify options={{render: DetailsLink}}>
272 {event.description}
273 </Linkify>
274 </Typography>
275 )}
276 </Box>
277 )}
278 {(isEditing || event.lang) && (
279 <Box pt={2} pr={1.5}>
280 <Typography variant="overline">
281 {t('event.fields.lang')}
282 </Typography>
283 {isEditing ? (
284 <LangSelector
285 value={event.lang}
286 onChange={lang => setEventUpdate({lang})}
287 />
288 ) : (
289 <Typography id="EventLang" sx={{pr: 3}}>
290 {langLocales[event.lang]}
291 </Typography>
292 )}
293 </Box>
294 )}
295 {!isEditing && (
296 <ShareEvent
297 title={`Caroster ${event.name}`}
298 sx={{width: '100%', mt: 2}}
299 />
300 )}
301 </Card>
302 </Container>
303 </Box>
304 );
305};
306
307export const getServerSideProps = pageUtils.getServerSideProps(
308 async (context, apolloClient) => {
309 const {uuid} = context.query;
310 const {host = ''} = context.req.headers;
311 let event = null;
312
313 // Fetch event
314 try {
315 const {data} = await apolloClient.query({
316 query: EventByUuidDocument,
317 variables: {uuid},
318 });
319 event = data?.eventByUUID?.data;
320 } catch (error) {
321 return {
322 notFound: true,
323 };
324 }
325
326 return {
327 props: {
328 eventUUID: uuid,
329 metas: {
330 title: event?.attributes?.name || '',
331 url: `https://${host}${context.resolvedUrl}`,
332 },
333 },
334 };
335 }
336);
337export default Page;